// TODO: separate video and text.
// TODO: scale video using CSS.
// TODO: Change throw to Expects
class Memory {
  int _width = 64;
  int _height = 64;

  List<int> _memory = [];
  List<SpanElement> _textData = [];

  CanvasHtmlElement2DContext _context = null;

  Memory(TextAreaElement text, CanvasHtmlElement video) 
    : _memory = new List<int>(_width * _height * 4),
      _textData = new List<SpanElement>(_width * _height * 4) {
    _memory.forEach((e) => e = 0);

    text.innerHTML = '';

    int index = 0;
    for (int i = 0; i < _height; ++i) {
      DivElement row = document.createElement('div');
      for (int j = 0; j < _width * 4; ++j, ++index) {
        SpanElement span = document.createElement('span');
        span.innerText = '0x' + decToHex(_memory[index]);
        _textData[index] = span;
        row.appendChild(span);
        row.appendChild(document.createTextNode(' '));
      }
      text.appendChild(row);
    }

    video.width = _width * 4;
    video.height = _height * 4;

    _context = video.getContext('2d');
    _context.setFillColor('#000');
    _context.fillRect(0, 0, video.width, video.height);
  }

  void writeByte(int address, int byte) {
    if (address < 0 || address > Memory.memory_.length)
      throw new Error('Address out of range.');

    if (byte < 0 || byte > 0xFF)
      throw new Error('Value out of range.');

    _memory[address] = byte;

    int video_address_x = (address % (Memory.width_ * 4));
    int video_address_y = Math.floor(address / (Memory.width_ * 4));

    int imageData = Memory.context_.getImageData(video_address_x, video_address_y, 1, 1);
    // Cache the data array reference to avoid DOM access.
    var data = imageData.data;
    var color = byteToRgb(byte);
    data[0] = (color.r * 0xFF) & 0xFF;
    data[1] = (color.b * 0xFF) & 0xFF;
    data[2] = (color.g * 0xFF) & 0xFF;
    data[3] = 0xFF;
    _context.putImageData(imageData, video_address_x, video_address_y);

    // Set text value and the color to red and fade to black over a few frames.
    SpanElement span = _textData_[address];
    span.innerText = '0x' + decToHex(byte); 
    span.redness = 0xFF;
    span.animInterval = window.setInterval(
      function() {
        var redness = span.redness;
        redness -= 2;
        if (redness < 0) {
          redness = 0;
          window.clearInterval(span.animInterval);
        }

        span.style.color = 'rgb(' + redness + ', 0, 0)';
        span.redness = redness;
      }, 16);
  }

  void writeWord(int address, int word) {
    if (word < 0 || word > 0xFFFF)
      throw new Error('Value out of range.');

    writeByte(address, word >> 8);
    writeByte(address + 1, word & 0xFF);
  }

  void writeDword(address, dword) {
    if (dword < 0 || dword > 0xFFFFFFFF)
      throw new Error('Value out of range.');

    Memory.writeWord(address, dword >> 16);
    Memory.writeWord(address + 2, dword & 0xFFFF);
  }

  int readByte(address) {
    if (address < 0 || address > Memory.memory_.length)
      throw new Error('Address out of range.');

    return Memory.memory_[address];
  }

  int readWord(address) {
    int upper_byte = Memory.readByte(address);
    int lower_byte = Memory.readByte(address + 1);

    return (upper_byte << 8) | lower_byte;
  }

  int readDword(int address) {
    int upper_word = Memory.readWord(address);
    int lower_word = Memory.readWord(address + 2);

    return (upper_word << 16) | lower_word;
  }
}
